﻿//************************************************************************************************************
// CoNatural.Data.dll
// Author: Roger Torres
//************************************************************************************************************
// You can use, modify, and distribute the source code and executable programs based on the source code.
// This source code is provided "as is" and without warranties as to performance or merchantability. 
// The author and/or distributors of this source code may have made statements about this source code. 
// Any such statements do not constitute warranties and shall not be relied on by the user in deciding 
// whether to use this source code. This source code is provided without any express or implied warranties 
// whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, 
// no warranty of fitness for a particular purpose is offered. The user is advised to test the source code 
// thoroughly before relying on it. The user must assume the entire risk of using the source code.
//************************************************************************************************************

using System;
using System.Collections.Generic;
using System.Data;
#if NET40
using System.Linq.Expressions;
#else
using Microsoft.Scripting.Ast;
#endif
using System.Reflection;
using CoNatural.Binders;

namespace CoNatural.Data.Binders {
	/// <summary>
	/// Field mapping
	/// </summary>
	public class FieldMapping : MappingBase {
		public int FieldIndex { get; private set; }
		public bool IsNullable { get; private set; }
		public Type TypeCast { get; private set; }
		public string GetMethod { get; private set; }

		protected internal FieldMapping(string source, string target, int fieldIndex, bool isNullable, Type typeCast, string getMethod)
			: base(source, target, null) {
			FieldIndex = fieldIndex;
			IsNullable = isNullable;
			TypeCast = typeCast;
			GetMethod = getMethod;
		}
	}

	/// <summary>
	/// To be used by router when materializing objects from data readers
	/// </summary>
	public class MaterializerBinderDelegateFactory : IBinderDelegateFactory {
		// Binding flags target types
		internal static readonly BindingFlags TargetFlags = BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.IgnoreCase;

		#region IBinderDelegateFactory Members

		public IBinder CreateBinder(object source, Type targetType) {
			var record = source as IDataRecord;
			if (record == null)
				throw new Exception("Source must implement IDataRecord");

			var mappings = GetMappings(record, targetType);
			var sourceParam = Expression.Parameter(typeof(object), "source");
			var targetParam = Expression.Parameter(typeof(object), "target");
			var binderBlock = GetBinderBlock(targetType, sourceParam, targetParam, mappings);
			var mapperBlock = GetMapperBlock(mappings);
			return new BinderBase(
				Expression.Lambda<BinderDelegate>(binderBlock, sourceParam, targetParam).Compile(),
				Expression.Lambda<MapperDelegate>(mapperBlock, sourceParam).Compile()
			);
		}

		#endregion

		#region Binding

		/// <summary>
		/// Generates mappings from source to target type.
		/// </summary>
		/// <param name="record">The source record.</param>
		/// <param name="targetType">The target type.</param>
		/// <returns>The mappings.</returns>
		private IList<FieldMapping> GetMappings(IDataRecord record, Type targetType) {
			var mappings = new List<FieldMapping>();
			for (int fieldIndex = 0; fieldIndex < record.FieldCount; fieldIndex++) {
				var targetProperty = targetType.GetProperty(record.GetName(fieldIndex), TargetFlags);
				if (targetProperty != null) {
					var mapping = GetMapping(record, targetProperty, fieldIndex);
					if (mapping != null)
						mappings.Add(mapping);
				}
			}
			return mappings;
		}

		protected internal virtual FieldMapping GetMapping(IDataRecord record, PropertyInfo targetProperty, int fieldIndex) {
			object[] attributes = targetProperty.GetCustomAttributes(typeof(IgnoreParameterAttribute), false);
			if (attributes.Length > 0) // not bindable
				return null;

			Type typeCast = targetProperty.PropertyType;
			if (!record.GetFieldType(fieldIndex).IsCastableTo(typeCast))
				return null;

			string getMethod = record.GetFieldType(fieldIndex).Name;
			bool isNullable = false;

			if (targetProperty.PropertyType == typeof(string)) {
				isNullable = true;
			}
			else if (record.GetFieldType(fieldIndex) == typeof(byte[])) {
				getMethod = "Value";
			}
			else if (targetProperty.PropertyType.IsNullable()) {
				isNullable = true;
			}
			else {
				// SQL 2008 types
				switch (getMethod) {
					case "SqlHierarchyId":
					case "SqlGeography":
					case "SqlGeometry":
						getMethod = "Value";
						break;
				}
			}

			return new FieldMapping(targetProperty.Name, targetProperty.Name, fieldIndex, isNullable, typeCast, getMethod);
		}

		#endregion Binding

		#region Expressions

		private BlockExpression GetBinderBlock(Type targetType, ParameterExpression sourceParam, ParameterExpression targetParam, IList<FieldMapping> mappings) {
			var end = Expression.Label(typeof(object), "end");

			if (mappings.Count > 0) {
				var source = Expression.Variable(typeof(IDataRecord), "s");
				var target = Expression.Variable(targetType, "t");

				return Expression.Block(
					new[] { source, target },
					Expression.Assign(source, Expression.Convert(sourceParam, typeof(IDataRecord))),
					Expression.Assign(target, Expression.Convert(targetParam, targetType)),
					Expression.IfThen(Expression.Equal(target, Expression.Constant(null)), Expression.Assign(target, Expression.New(targetType))),
					Expression.Block(GetBinderExpressions(mappings, source, target)),
					Expression.Label(end, target)
				);
			}
			else
				return Expression.Block(
					Expression.Label(end, targetParam)
				);
		}

		private IEnumerable<Expression> GetBinderExpressions(IList<FieldMapping> mappings, Expression source, Expression target) {
			foreach (var mapping in mappings) {
				var getter = Expression.Call(source, typeof(IDataRecord).GetMethod(string.Concat("Get", mapping.GetMethod)), Expression.Constant(mapping.FieldIndex));
				var assignval = Expression.Assign(Expression.Property(target, mapping.Target), Expression.Convert(getter, mapping.TypeCast));
				var assignnil = Expression.Assign(Expression.Property(target, mapping.Target), Expression.Convert(Expression.Constant(null), mapping.TypeCast));
				if (mapping.IsNullable) {
					// check for DBNull when field is nullable
					yield return Expression.IfThenElse(Expression.Call(source, typeof(IDataRecord).GetMethod("IsDBNull"), Expression.Constant(mapping.FieldIndex)), 
						assignnil, assignval);
				}
				else
					yield return assignval;
			}
		}

		private BlockExpression GetMapperBlock(IList<FieldMapping> mappings) {
			var maplist = Expression.Variable(typeof(List<IMapping>), "mappings");
			var end = Expression.Label(typeof(List<IMapping>), "end");

			if (mappings.Count > 0) {
				return Expression.Block(
					new[] { maplist },
					Expression.Assign(maplist, Expression.New(typeof(List<IMapping>))),
					Expression.Block(GetMapperExpressions(mappings, maplist)),
					Expression.Label(end, maplist)
				);
			}
			else
				return Expression.Block(
					new[] { maplist },
					Expression.Assign(maplist, Expression.New(typeof(List<IMapping>))),
					Expression.Label(end, maplist)
				);
		}

		private IEnumerable<Expression> GetMapperExpressions(IList<FieldMapping> mappings, Expression maplist) {
			foreach (var mapping in mappings) {
				// create expression to construct new IMapping from mapping
				var constructor = typeof(MappingBase).GetConstructor(new[] { typeof(string), typeof(string), typeof(object) });
				var map = Expression.New(
					constructor,
					Expression.Constant(mapping.Source),
					Expression.Constant(mapping.Target),
					Expression.Constant(null)
				);
				yield return Expression.Call(maplist, typeof(List<IMapping>).GetMethod("Add"), map);
			}
		}

		#endregion Expressions
	}
}
